package com.uit.httt04.duongvanlam.facedetection.algorithm;

import java.io.File;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

import android.annotation.SuppressLint;
import android.util.Log;

import static com.googlecode.javacv.cpp.opencv_core.*;

import com.googlecode.javacv.cpp.opencv_core.CvMat;
import com.uit.httt04.duongvanlam.facedetection.classes.DistanceAlgorithm;
import com.uit.httt04.duongvanlam.facedetection.classes.ImageAndFace;
import com.uit.httt04.duongvanlam.facedetection.classes.ObjectAlgorithm;
import com.uit.httt04.duongvanlam.facedetection.classes.RankAlgorithm;
import com.uit.httt04.duongvanlam.facedetection.utils.Configuration;
import com.uit.httt04.duongvanlam.facedetection.utils.Matrix;

@SuppressLint(
{ "DefaultLocale", "UseSparseArrays" })
public abstract class Algorithm
{
	//
	// <-- path/to/training folder
	//
	private String trainingFolder;
	//
	// <-- List Object
	//
	private List<ObjectAlgorithm> listObjects;
	//
	// <-- Filtered Object
	//
	private List<ObjectAlgorithm> projectedListObjects;
	//
	// <-- Image's heigh
	//
	private int height;
	//
	// <-- Image's width
	//
	private int width;
	//
	// <-- Allowed extension examples: .JPG
	//
	private List<String> allowedExt;
	//
	// <-- <"Join",0>, <"Susu",1>, <"Michale",2>
	//
	private Map<Integer, String> autoIdToName;
	//
	// <-- Rank Object
	//
	private List<RankAlgorithm> rankedObjects;
	//
	// <-- Rank Object
	//
	private List<RankAlgorithm> rankedSingleObject;
	//
	// <--- Eigenvector
	//
	private CvMat eigenvect;
	//
	//
	//
	private CvMat eigenVal;
	//
	// <-- Filtered Eigen Vector
	//
	private CvMat projectedEigenvect;
	//
	// <-- First N Eigenvector
	//
	private int nEigenvector;

	public Algorithm()
	{
		this.setAllowedExt(new ArrayList<String>());
		this.getAllowedExt().add(Configuration.AllowedExt.toUpperCase());
		this.setListObjects(new ArrayList<ObjectAlgorithm>());
		setAutoIdToName(new HashMap<Integer, String>());
	}

	public String getTrainingFolder()
	{
		return trainingFolder;
	}

	public void setTrainingFolder(String trainingFolder)
	{
		this.trainingFolder = trainingFolder;
	}

	public List<ObjectAlgorithm> getListObjects()
	{
		return listObjects;
	}

	public int getAmountOfFaces()
	{
		if (this.getAutoIdToName() != null)
			return this.getAutoIdToName().size();
		else
			return 0;
	}

	public void setListObjects(List<ObjectAlgorithm> listObject)
	{
		this.listObjects = listObject;
	}

	public int getAmountOfObjects()
	{
		if (this.getListObjects() != null)
			return this.getListObjects().size();
		else
			return 0;
	}

	public List<ObjectAlgorithm> getProjectedListObjects()
	{
		return projectedListObjects;
	}

	public int getAmountOfProjectedObjects()
	{
		if (this.getProjectedListObjects() != null)
			return this.getProjectedListObjects().size();
		else
			return 0;
	}

	public void setProjectedListObjects(List<ObjectAlgorithm> projectedListObject)
	{
		this.projectedListObjects = projectedListObject;
	}

	public int getHeight()
	{
		return height;
	}

	public void setHeight(int height)
	{
		this.height = height;
	}

	public int getWidth()
	{
		return width;
	}

	public void setWidth(int width)
	{
		this.width = width;
	}

	public List<String> getAllowedExt()
	{
		return allowedExt;
	}

	public void setAllowedExt(List<String> allowedExt)
	{
		this.allowedExt = allowedExt;
	}

	public Map<Integer, String> getAutoIdToName()
	{
		return autoIdToName;
	}

	public void setAutoIdToName(Map<Integer, String> autoIdToName)
	{
		this.autoIdToName = autoIdToName;
	}

	public List<RankAlgorithm> getRankedObjects()
	{
		return rankedObjects;
	}

	public void setRankedObjects(List<RankAlgorithm> rankedObjects)
	{
		this.rankedObjects = rankedObjects;
	}

	public List<RankAlgorithm> getRankedSingleObject()
	{
		return rankedSingleObject;
	}

	public void setRankedSingleObject(List<RankAlgorithm> rankedSingleObject)
	{
		this.rankedSingleObject = rankedSingleObject;
	}

	public CvMat getEigenvect()
	{
		return eigenvect;
	}

	public void setEigenvect(CvMat eigenvect)
	{
		this.eigenvect = eigenvect;
	}

	public CvMat getProjectedEigenvect()
	{
		return projectedEigenvect;
	}

	public void setProjectedEigenvect(CvMat projectedEigenvect)
	{
		this.projectedEigenvect = projectedEigenvect;
	}

	public int getNEigenvector()
	{
		return nEigenvector;
	}

	public void setNEigenvector(int nEigenvector)
	{
		this.nEigenvector = nEigenvector;
	}

	public CvMat getEigenVal()
	{
		return eigenVal;
	}

	public void setEigenVal(CvMat eigenVal)
	{
		this.eigenVal = eigenVal;
	}

	public int getIDByValue(String Name)
	{
		try
		{
			for (Map.Entry<Integer, String> e : getAutoIdToName().entrySet())
			{
				if (e.getValue().equalsIgnoreCase(Name))
				{
					return e.getKey();
				}
			}
		}
		catch (Exception e)
		{
			e.printStackTrace();
		}

		return -1;
	}

	@SuppressWarnings("unused")
	private int countElementByID(int id)
	{
		int count = 0;
		for (int i = 0; i < this.getProjectedListObjects().size(); i++)
		{
			if (this.projectedListObjects.get(i).getID() == id)
				count++;
		}

		return count;
	}

	public abstract void Train() throws Exception;

	public void LoadFromAlgorithm(Algorithm algorithm)
	{
		this.setTrainingFolder(algorithm.getTrainingFolder());
		this.setHeight(algorithm.getHeight());
		this.setWidth(algorithm.getWidth());
		this.setAutoIdToName(algorithm.getAutoIdToName());
		this.setListObjects(algorithm.getListObjects());
	}

	public void Load(String folderPath, int height, int width) throws Exception
	{
		try
		{
			this.setTrainingFolder(folderPath);
			this.setHeight(height);
			this.setWidth(width);

			//
			// <-- Folder's Path
			//
			Log.d(this.getClass().getName(), "Load: " + folderPath);
			//
			// Folder's Path -->
			//

			File folder = new File(folderPath);
			if (folder.exists() == false)
				throw new Exception("Not found folder");
			File[] files = folder.listFiles();

			//
			// <-- Amount Files
			//
			Log.d(this.getClass().getName(), "Amount Files: " + files.length);
			//
			// Amount Files -->
			//

			String fileName = "";
			String filePath = "";
			String fileExt = "";
			int autoNumberPersonId = 0;

			//
			// <-- Add data into list
			//
			for (int i = 0; i < files.length; i++)
			{
				if (files[i].isFile() == true)
				{
					fileName = files[i].getName();
					filePath = files[i].getAbsolutePath();
					fileExt = filePath.substring(filePath.lastIndexOf(".") + 1);
					fileExt = fileExt.toUpperCase();

					if (fileName != null && fileName != "" && this.getAllowedExt().contains(fileExt) == true)
					{
						// lam_1.jpg, lam_2.jpg, lam_3.jpg, thuy_1.jpg,
						// thuy_2.jpg
						// ...
						String PersonName = ImageAndFace.getPersonNameByImageName(fileName);
						if (PersonName != null && PersonName.equalsIgnoreCase("") == false)
						{
							int curID = -1;
							if (getAutoIdToName().containsValue(PersonName) == false)
							{
								Log.d(this.getClass().getName(), "Name: " + PersonName + ",ID: " + autoNumberPersonId);
								this.getAutoIdToName().put(autoNumberPersonId, PersonName);
								curID = autoNumberPersonId;
								autoNumberPersonId++;
							}
							else
							{
								curID = this.getIDByValue(PersonName);
							}

							ObjectAlgorithm temp = new ObjectAlgorithm(filePath, curID, height, width);
							if (temp != null)
								this.getListObjects().add(temp);
						}
					}
				}
			}
			//
			// Add data into list -->
			//

		}
		catch (Exception e)
		{
			throw e;
		}
	}

	public void Project(int[] n) throws Exception
	{
		if (n.length == 1 && n[0] > 0 && n[0] <= this.getWidth())
		{
			this.setNEigenvector(n[0]);
			//
			// <-- Get n EigenVector
			//
			CvMat projectedEigenVectArrT = cvCreateMat(this.getNEigenvector(), this.getWidth(), CV_32FC1);
			cvSetZero(projectedEigenVectArrT);
			for (int i = 0; i < this.getNEigenvector(); i++)
				for (int j = 0; j < this.getWidth(); j++)
				{
					projectedEigenVectArrT.put(i, j, this.getEigenvect().get(i, j));
				}
			//
			// Get n EigenVector -->
			//

			//
			// <-- filteredEigenVectArr transpose
			//
			if (this.getProjectedEigenvect() != null)
				cvReleaseMat(this.getProjectedEigenvect());
			// [z1,z2,z3,z4,z5, ..., Z"nEigenvector"]
			this.setProjectedEigenvect(cvCreateMat(this.getWidth(), this.getNEigenvector(), CV_32FC1));
			cvTranspose(projectedEigenVectArrT, getProjectedEigenvect());
			//
			// filteredEigenVectArr transpose -->
			//

			//
			// <-- Image's Matrix be projected
			//

			//
			// Image's Matrix be projected -->
			//

			//
			// <-- List Image be projected
			//
			if (this.getProjectedListObjects() == null)
			{
				this.setProjectedListObjects(new ArrayList<ObjectAlgorithm>());
			}
			this.getProjectedListObjects().clear();
			//
			// List Image be projected -->
			//

			CvMat projectedMatrix = cvCreateMat(this.getHeight(), this.getNEigenvector(), CV_32FC1);

			for (int i = 0; i < this.getListObjects().size(); i++)
			{
				cvSetZero(projectedMatrix);
				cvMatMul(this.getListObjects().get(i).getMatrix(), getProjectedEigenvect(), projectedMatrix);
				//
				// <-- Add to list be projected
				//
				ObjectAlgorithm projectedObject = new ObjectAlgorithm(projectedMatrix, this.getListObjects().get(i).getID(), this.getListObjects().get(i).getImagePath());
				this.getProjectedListObjects().add(projectedObject);
				//
				// Add to list be projected -->
				//
			}

			//
			// <-- Free
			//
			cvReleaseMat(projectedMatrix);
			cvReleaseMat(projectedEigenVectArrT);
			//
			// Free -->
			//
		}
		else
		{
			throw new Exception("Wrong input");
		}
	}

	// coefficientAlgorithm in [0,1]
	public CvMat Test(String imagePath, double coefficientAlgorithm, int maxScore) throws Exception
	{
		try
		{
			if (this.getProjectedEigenvect() == null || this.getProjectedListObjects() == null || this.getEigenVal() == null || getProjectedListObjects().size() < 1)
				throw new Exception("Haven't been projected");
			else
			{
				String fileExt = imagePath.substring(imagePath.lastIndexOf(".") + 1);
				fileExt = fileExt.toUpperCase();

				if (this.getAllowedExt().contains(fileExt) == true)
				{
					// Convert image into object in algorithm
					ObjectAlgorithm imageTest = new ObjectAlgorithm(imagePath, -1, this.getHeight(), this.getWidth());

					// Project image
					CvMat projectedImageTestMatrix = cvCreateMat(this.getHeight(), this.getNEigenvector(), CV_32FC1);
					cvMatMul(imageTest.getMatrix(), this.getProjectedEigenvect(), projectedImageTestMatrix);

					double distanceCurObject;
					double idCurObject;

					CvMat distanceResult = cvCreateMat(1, this.getProjectedListObjects().size(), CV_64FC1);
					cvSetZero(distanceResult);
					CvMat sortedResultDistance = cvCreateMat(1, this.getProjectedListObjects().size(), CV_64FC1);
					cvSetZero(sortedResultDistance);

					CvMat facesScore = cvCreateMat(1, this.getAmountOfFaces(), CV_64FC1);
					cvSetZero(facesScore);
					//CvMat temporaryFacesScore = cvCreateMat(1, this.getAmountOfFaces(), CV_64FC1); // Version 1
					//cvSetZero(temporaryFacesScore); // Version 1

					//CvMat projectedImageTestMatrixNth = cvCreateMat(projectedImageTestMatrix.rows(), 1, CV_32FC1); // Version 1
					//cvSetZero(projectedImageTestMatrixNth); // Version 1
					//CvMat getProjectedListObjectsNth = cvCreateMat(projectedImageTestMatrix.rows(), 1, CV_32FC1); // Version 1
					//cvSetZero(getProjectedListObjectsNth); // Version 1

					/*
					// Version 1
					for (int j = 0; j < this.getNEigenvector(); j++)
					{
						cvSetZero(distanceResult);
						// get N th Column
						cvGetCol(projectedImageTestMatrix, projectedImageTestMatrixNth, j);
						// Loop list objects
						for (int i = 0; i < this.getProjectedListObjects().size(); i++)
						{
							cvGetCol(this.getProjectedListObjects().get(i).getMatrix(), getProjectedListObjectsNth, j);
							// Calculate distance
							distanceCurObject = Matrix.DistanceByCol(projectedImageTestMatrixNth, getProjectedListObjectsNth);
							distanceCurObject = (int) distanceCurObject;
							idCurObject = (this.getProjectedListObjects().get(i).getID() + Configuration.rounded) / Configuration.round2Decimal;
							cvSetReal2D(distanceResult, 0, i, distanceCurObject + idCurObject);
						}
						// Sort
						cvSort(distanceResult, sortedResultDistance, null, 0);
						//
						cvSetZero(temporaryFacesScore);
						int minDistance = 0;
						// Set score
						for (int i = 0; i < sortedResultDistance.cols(); i++)
						{
							double temporaryDouble = cvGetReal2D(sortedResultDistance, 0, i);
							int curDistance = (int) temporaryDouble;
							int curId = (int) ((temporaryDouble - curDistance) * Configuration.round2Decimal);
							int curScore = (int) cvGetReal2D(temporaryFacesScore, 0, curId);
							if (curScore == 0)
							{
								double newScore;
								if (i == 0)
								{
									newScore = maxScore;
									minDistance = curDistance;
								}
								else
								{
									newScore = ((double) (minDistance * maxScore)) / (double) curDistance;
								}
								//
								newScore = newScore * coefficientAlgorithm;
								//newScore = newScore / this.getNEigenvector();
								newScore = (int) newScore;
								cvSetReal2D(temporaryFacesScore, 0, curId, newScore);
							}
						}
						cvAdd(temporaryFacesScore, facesScore, facesScore, null);
					}
					// Version 1
					*/

					// <-- Version 2
					for (int i = 0; i < this.getProjectedListObjects().size(); i++)
					{
						distanceCurObject = Matrix.DistanceByCol(projectedImageTestMatrix, this.getProjectedListObjects().get(i).getMatrix());
						distanceCurObject = (int) distanceCurObject;
						idCurObject = (this.getProjectedListObjects().get(i).getID() + Configuration.rounded) / Configuration.round2Decimal;
						cvSetReal2D(distanceResult, 0, i, distanceCurObject + idCurObject);
					}

					cvSort(distanceResult, sortedResultDistance, null, 0);

					int minDistance = 0;
					for (int i = 0; i < sortedResultDistance.cols(); i++)
					{
						double temporaryDouble = cvGetReal2D(sortedResultDistance, 0, i);
						int curDistance = (int) temporaryDouble;
						int curId = (int) ((temporaryDouble - curDistance) * Configuration.round2Decimal);
						int curScore = (int) cvGetReal2D(facesScore, 0, curId);
						if (curScore == 0)
						{
							double newScore;
							if (i == 0)
							{
								newScore = maxScore;
								minDistance = curDistance;
							}
							else
							{
								newScore = ((double) (minDistance * maxScore)) / (double) curDistance;
							}
							//
							newScore = newScore * coefficientAlgorithm;
							newScore = (int) newScore;
							cvSetReal2D(facesScore, 0, curId, newScore);
						}
					}
					// Version 2 -->

					cvReleaseMat(distanceResult);
					cvReleaseMat(projectedImageTestMatrix);
					//cvReleaseMat(projectedImageTestMatrixNth); // Version 1
					//cvReleaseMat(getProjectedListObjectsNth); // Version 1
					//cvReleaseMat(temporaryFacesScore); // Version 1
					cvReleaseMat(sortedResultDistance);
					fileExt = null;
					try
					{
						imageTest.finalize();
					}
					catch (Throwable e)
					{
						e.printStackTrace();
					}
					imageTest = null;
					//
					return facesScore;
				}
				else
					throw new Exception("Extension doesn't allow");

			}
		}
		catch (Exception e)
		{
			throw e;
		}
	}

	public List<RankAlgorithm> Test(String imagePath, int nInput) throws Exception
	{
		try
		{
			if (this.projectedEigenvect == null || this.getProjectedListObjects() == null || this.getProjectedListObjects().size() < 1)
				throw new Exception("Haven't been projected");
			else
			{
				String fileExt = imagePath.substring(imagePath.lastIndexOf(".") + 1);
				fileExt = fileExt.toUpperCase();

				if (this.allowedExt.contains(fileExt) == true)
				{
					ObjectAlgorithm imageTest = new ObjectAlgorithm(imagePath, -1, this.height, this.width);

					CvMat projectedMatrix = cvCreateMat(this.height, this.nEigenvector, CV_32FC1);
					cvMatMul(imageTest.getMatrix(), this.projectedEigenvect, projectedMatrix);

					DistanceAlgorithm[] result = new DistanceAlgorithm[this.getProjectedListObjects().size()];
					double distanceCurObject;

					CvMat projectedImageTestMatrixNth = cvCreateMat(this.getWidth(), nInput, CV_32FC1);
					cvGetCols(projectedMatrix, projectedImageTestMatrixNth, 0, nInput);
					for (int i = 0; i < this.getProjectedListObjects().size(); i++)
					{
						CvMat projectedListObjectsMatrixNth = cvCreateMat(this.getWidth(), nInput, CV_32FC1);
						cvGetCols(this.getProjectedListObjects().get(i).getMatrix(), projectedListObjectsMatrixNth, 0, nInput);
						//
						distanceCurObject = Matrix.Distance(projectedImageTestMatrixNth, projectedListObjectsMatrixNth);
						result[i] = new DistanceAlgorithm(distanceCurObject, this.getProjectedListObjects().get(i).getImageName(), this.getProjectedListObjects().get(i).getID());
						cvReleaseMat(projectedListObjectsMatrixNth);
					}
					cvReleaseMat(projectedImageTestMatrixNth);
					DistanceAlgorithm.SortResult(result);

					if (this.rankedObjects == null)
						this.rankedObjects = new ArrayList<RankAlgorithm>();
					this.rankedObjects.clear();

					for (int i = 0; i < result.length; i++)
					{
						RankAlgorithm rankedObjectTemp = new RankAlgorithm(i + 1, result[i].getImageName(), result[i].getID());
						this.rankedObjects.add(rankedObjectTemp);
					}

					// Free
					try
					{
						imageTest.finalize();
					}
					catch (Throwable e)
					{
						e.printStackTrace();
					}
					imageTest = null;
					fileExt = null;
					result = null;
					cvReleaseMat(projectedMatrix);
					// Return
					return this.rankedObjects;
				} // End allowed extension
				else
				{
					throw new Exception("Extension doesn't allow");
				}
			} // End have been projected
		} // End Try
		catch (Exception e)
		{
			throw e;
		}
	}

	@SuppressWarnings("unused")
	private CvMat NormalizeVector(CvMat eigenVectArr) throws Exception
	{
		try
		{
			CvMat result = cvCreateMat(eigenVectArr.rows(), eigenVectArr.cols(), CV_32FC1);
			cvSetZero(result);
			CvMat Scale = cvCreateMat(eigenVectArr.rows(), 1, CV_32FC1);
			cvSetZero(Scale);

			float temp;
			for (int i = 0; i < eigenVectArr.rows(); i++)
			{
				temp = 0;
				for (int j = 0; j < eigenVectArr.cols(); j++)
				{
					temp = (float) (temp + Math.pow(eigenVectArr.get(i, j), 2));
				}
				temp = (float) Math.pow(temp, 1 / 2);
				Scale.put(i, 0, temp);
			}

			for (int i = 0; i < eigenVectArr.rows(); i++)
			{
				for (int j = 0; j < eigenVectArr.cols(); j++)
				{
					result.put(i, j, eigenVectArr.get(i, j) / Scale.get(i, 0));
				}
			}

			cvReleaseMat(Scale);

			return result;
		}
		catch (Exception e)
		{
			throw e;
		}
	}

	protected int[] CountElementEachClass(int amountClasses, List<ObjectAlgorithm> ListObject) throws Exception
	{
		try
		{
			int[] result;
			result = new int[amountClasses];
			for (int i = 0; i < amountClasses; i++)
				result[i] = 0;

			for (int i = 0; i < ListObject.size(); i++)
			{
				int ID = ListObject.get(i).getID();
				result[ID]++;
			}
			return result;
		}
		catch (Exception e)
		{
			throw e;
		}
	}
}
